package com.yanggu.common.operatorlog;


import com.alibaba.ttl.TransmittableThreadLocal;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yanggu.common.exception.IException;
import com.yanggu.common.util.AopDataUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import cn.hutool.v7.extra.spring.SpringUtil;
import cn.hutool.v7.http.server.servlet.ServletUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static com.yanggu.common.operatorlog.BusinessStatusEnum.FAIL;
import static com.yanggu.common.operatorlog.BusinessStatusEnum.SUCCESS;

/**
 * 操作日志切面
 */
@Slf4j
@Aspect
@Component
@ConditionalOnProperty(name = "web.operator-log.enable", havingValue = "true", matchIfMissing = true)
public class OperatorLogAspect {

    @Value("${spring.application.name:}")
    private String applicationName;

    @Autowired
    private HttpServletRequest request;

    @Autowired
    private HttpServletResponse response;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private ApplicationContext applicationContext;

    private static final ThreadLocal<OpsLogEvent> LOG_EVENT_CACHE = new TransmittableThreadLocal<>();

    /**
     * 处理请求前执行
     */
    @Before("@annotation(OperateLog)")
    public void doBefore() {
        OpsLogEvent opsLogEvent = new OpsLogEvent();
        opsLogEvent.setStartTime(new Date());
        opsLogEvent.setEventTime(opsLogEvent.getStartTime());
        LOG_EVENT_CACHE.set(opsLogEvent);
    }

    /**
     * 处理完请求后执行（无异常执行）
     */
    @AfterReturning(pointcut = "@annotation(OperateLog)", returning = "result")
    public void doAfterReturning(JoinPoint joinPoint, Object result) throws Exception {
        buildLog(joinPoint, result, null);
    }

    /**
     * 拦截异常操作（有异常执行）
     */
    @AfterThrowing(pointcut = "@annotation(OperateLog)", throwing = "throwable")
    public void exception(JoinPoint joinPoint, Throwable throwable) throws Exception {
        buildLog(joinPoint, null, throwable);
    }

    private void buildLog(JoinPoint joinPoint, Object result, Throwable throwable) throws Exception {
        try {
            OpsLogEvent opsLogEvent = LOG_EVENT_CACHE.get();
            Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
            OperateLog operateLog = method.getAnnotation(OperateLog.class);
            assert operateLog != null;
            //如果不为空，发生异常了
            if (throwable != null) {
                //设置失败
                opsLogEvent.setStatus(FAIL.getCode());
                //如果是业务异常，则设置异常码和异常信息
                if (throwable instanceof IException exception) {
                    //设置异常码
                    opsLogEvent.setExceptionCode(exception.getCode());
                    //设置异常信息
                    opsLogEvent.setExceptionMessage(exception.getMessage());
                } else {
                    //设置异常信息
                    opsLogEvent.setExceptionMessage(throwable.getMessage());
                }
                //设置异常类名
                opsLogEvent.setExceptionClassName(throwable.getClass().getName());
            } else {
                //设置成功
                opsLogEvent.setStatus(SUCCESS.getCode());
                //保存响应参数
                String responseJsonData = buildResponseData(result, operateLog);
                opsLogEvent.setResponseData(responseJsonData);

                //设置具体的操作内容
                String specialOperationInfo = buildOperateContent(joinPoint);
                opsLogEvent.setOperateContent(specialOperationInfo);
            }
            //设置结束时间
            opsLogEvent.setEndTime(new Date());
            //设置耗时
            opsLogEvent.setCost(opsLogEvent.getEndTime().getTime() - opsLogEvent.getStartTime().getTime());
            //设置应用名
            opsLogEvent.setApplicationName(applicationName);
            //设置name
            opsLogEvent.setName(operateLog.name());
            //设置操作类型
            opsLogEvent.setOperateType(operateLog.operateType().getType());
            //保存请求参数
            String requestParam = buildRequestData(joinPoint, operateLog);
            opsLogEvent.setRequestParam(requestParam);
            //设置请求方式
            opsLogEvent.setRequestMethod(request.getMethod());
            //设置请求的uri
            opsLogEvent.setUri(request.getRequestURI());
            //设置浏览器UA标识
            opsLogEvent.setUserAgent(ServletUtil.getHeaderIgnoreCase(request, "User-Agent"));
            //设置请求头
            Map<String, List<String>> reqCollect = ServletUtil.getHeadersMap(request).entrySet().stream()
                    .collect(Collectors.toMap(Map.Entry::getKey, tempEntry -> new ArrayList<>(tempEntry.getValue())));
            opsLogEvent.setRequestHeaderMap(reqCollect);
            //设置响应头
            Map<String, List<String>> respCollect = ServletUtil.getHeadersMap(response).entrySet().stream()
                    .collect(Collectors.toMap(Map.Entry::getKey, tempEntry -> new ArrayList<>(tempEntry.getValue())));
            opsLogEvent.setResponseHeaderMap(respCollect);
            //设置ip
            opsLogEvent.setIp(ServletUtil.getClientIP(request));
            //设置执行方法
            opsLogEvent.setExecuteMethod(getExecuteMethod(joinPoint));
            log.info("[操作日志信息] 详细内容: {}", objectMapper.writeValueAsString(opsLogEvent));
            applicationContext.publishEvent(opsLogEvent);
        } finally {
            LOG_EVENT_CACHE.remove();
        }
    }

    private String buildRequestData(JoinPoint joinPoint, OperateLog operateLog) throws Exception {
        //如果需要保存请求参数, 则保存
        if (!operateLog.isSaveRequestData()) {
            return "";
        }
        String[] propertyArray = operateLog.excludeRequestParamNames();
        return AopDataUtil.buildRequestData(joinPoint, propertyArray, objectMapper);
    }

    private String buildResponseData(Object result, OperateLog operateLog) throws Exception {
        //如果需要保存响应参数, 则保存
        if (!operateLog.isSaveResponseData() || result == null) {
            return "";
        }
        String[] propertyArray = operateLog.excludeResponseFieldNames();
        return AopDataUtil.buildResponseData(result, propertyArray, objectMapper);
    }

    private String buildOperateContent(JoinPoint joinPoint) {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        OperateLog operateLog = method.getAnnotation(OperateLog.class);
        Class<? extends OperationFunction> function = operateLog.function();
        OperationFunction operationFunction = SpringUtil.getBean(function);
        String specialOperationInfo = "";
        if (operationFunction != null) {
            Object[] args = joinPoint.getArgs();
            specialOperationInfo = operationFunction.getSpecialOperationInfo(args);
        }
        return specialOperationInfo;
    }

    private String getExecuteMethod(JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();

        // 修改后的代码段
        Method method = signature.getMethod();
        String[] parameterNames = signature.getParameterNames();
        Class<?>[] parameterTypes = method.getParameterTypes();

        //将类型和参数名组合成 "类型 名称" 的格式
        List<String> paramList = new ArrayList<>();
        for (int i = 0; i < parameterTypes.length; i++) {
            paramList.add(parameterTypes[i].getName() + " " + parameterNames[i]);
        }
        String paramsSignature = String.join(", ", paramList);

        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        //拼接完整方法签名
        return className + "." + methodName + "(" + paramsSignature + ")";
    }

}
